﻿using Gargoyle.Roton;
using Gargoyle.ZZT;
using OpenTK;
using OpenTK.Input;
using OpenTK.Graphics.OpenGL;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Text;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace Lyon
{
    /// <summary>
    /// Runs Lyon using a simple OpenGL renderer.
    /// </summary>
    class OpenGLWindow : GameWindow
    {
        private Audio Audio;
        private string CurrentName;
        int GameWidth;
        int GameHeight;
        private bool Initialized = false;
        private BufferInput Input;
        private Color[] Palette;
        private TextModeVideo Video;
        private bool VideoBlink;
        private int VideoTick;
        private Game ZZT;

        private const int NUM_GLYPHS = 256;
        private int[] Glyphs;

        private bool AudioEnabled = true;

        private bool VideoBlinkOn = true;
        private string VideoFont = null;
        private string VideoPalette = null;
        private decimal VideoScale = 1;

        public OpenGLWindow() : base(640, 350) { }
        public OpenGLWindow(decimal Scale) : base((int)(640 * Scale), (int)(350 * Scale)) { }

        /// <summary>
        /// OpenGL setup and resource loading.
        /// </summary>
        protected override void OnLoad(EventArgs e)
        {
            LoadConfig();
            if(InitializeGame())
            {
                RegisterEvents();

                GL.ClearColor(Color.Black);

                // disable some useless OpenGL features :)
                GL.Disable(EnableCap.Lighting);
                GL.Disable(EnableCap.DepthTest);

                // load game palette
                Palette = Video.GetPalette();
                
                // generate the initial textures
                GL.Enable(EnableCap.Texture2D);
                byte[] fontData;
                int fontHeight;
                Glyphs = new int[NUM_GLYPHS];
                GL.GenTextures(NUM_GLYPHS, Glyphs);
                fontData = Video.GetFont();
                fontHeight = fontData.Length / NUM_GLYPHS;
                int texSize = 8 * fontHeight * 4;

                // generate textures for each font
                for(int i = 0; i < NUM_GLYPHS; i++)
                {
                    // create a 32-bit RGBA texture representing each glyph
                    byte[] textureData = new byte[texSize];
                    byte value;
                    byte row;
                    int bytePosition = 0;
                    IntPtr texPtr = Marshal.AllocHGlobal(texSize);

                    for(int y = 0; y < fontHeight; y++)
                    {
                        row = fontData[y + (fontHeight * i)];
                        for(int x = 0; x < 8; x++)
                        {
                            bytePosition = ((y * 8) + x) * 4;
                            value = ((row & (byte)0x80) != 0) ? (byte)0xFF : (byte)0x00;
                            textureData[bytePosition]
                                = textureData[bytePosition + 1]
                                = textureData[bytePosition + 2]
                                = textureData[bytePosition + 3] = value;
                            row <<= 1;
                        }
                    }

                    Marshal.Copy(textureData, 0, texPtr, texSize);
                    GL.BindTexture(TextureTarget.Texture2D, Glyphs[i]);
                    GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, 8, fontHeight, 0, OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, texPtr);
                    GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
                    GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);
                }

                GL.Ortho(0.0, 80.0, 25.0, 0.0, 0.0, 255.0);
                Initialized = true;
            }
            else
            {
                this.Exit();
            }
        }

        /// <summary>
        /// Handles all drawing logic.
        /// </summary>
        /// <pparam name="e">Contains the amount of time since the last rendered frame.</pparam>
        protected override void OnRenderFrame(FrameEventArgs e)
        {
            AnsiChar ansiChar;
            byte chColor, chChar;
            VideoSection screen;

            UpdateZZTVideo();
            screen = Video.GetSection(new Rectangle(0, 0, 80, 25));

            GL.Clear(ClearBufferMask.ColorBufferBit);
            GL.MatrixMode(MatrixMode.Projection);
            GL.LoadIdentity();

            // draw background
            GL.Disable(EnableCap.Blend);
            GL.Disable(EnableCap.Texture2D);
            for(int y = 0; y < 25; y++)
            {
                for(int x = 0; x < 80; x++)
                {
                    chColor = screen.Chars[(y * (Common.WideMode ? 2 : 1) * GameWidth) + x].Color;
                    chColor >>= 4;  // push off foreground color
                    if(Palette[chColor] == Color.Black) continue;

                    if(VideoBlinkOn && chColor >= 0x08)
                        GL.Color4(Palette[chColor - 0x08]);
                    else
                        GL.Color4(Palette[chColor]);
                    
                    GL.Begin(BeginMode.Quads);
                    if(!Common.WideMode)
                    {
                        GL.Vertex2(x    , y    );
                        GL.Vertex2(x + 1, y    );
                        GL.Vertex2(x + 1, y + 1);
                        GL.Vertex2(x    , y + 1);
                    }
                    else
                    {
                        GL.Vertex2(x * 2    , y    );
                        GL.Vertex2(x * 2 + 2, y    );
                        GL.Vertex2(x * 2 + 2, y + 1);
                        GL.Vertex2(x * 2    , y + 1);
                    }
                    GL.End();
                }
            }

            // overlay glyphs on top of backgrounds
            GL.Enable(EnableCap.Blend);
            GL.Enable(EnableCap.Texture2D);
            GL.BlendFunc(BlendingFactorSrc.SrcAlpha, BlendingFactorDest.OneMinusSrcAlpha);

            for(int y = 0; y < GameHeight; y++)
            {
                for(int x = 0; x < GameWidth; x++)
                {
                    ansiChar = screen.Chars[(y * (Common.WideMode ? 2 : 1) * GameWidth) + x];
                    chColor = ansiChar.Color;
                    chChar = ansiChar.Character;

                    // check to see if this character should be drawn
                    if((chColor & 0x0F) == (chColor >> 4)) continue;

                    // check to see if the character should blink
                    if(VideoBlinkOn && ((chColor >> 4) >= 0x08))
                        GL.Color4(GetBlinkColor(chColor));
                    else
                        GL.Color4(Palette[chColor & 0x0F]);

                    GL.BindTexture(TextureTarget.Texture2D, Glyphs[chChar]);

                    GL.Begin(BeginMode.Quads);
                    if(!Common.WideMode)
                    {
                        GL.TexCoord2(0.0f, 0.0f); GL.Vertex2(x    , y    );
                        GL.TexCoord2(1.0f, 0.0f); GL.Vertex2(x + 1, y    );
                        GL.TexCoord2(1.0f, 1.0f); GL.Vertex2(x + 1, y + 1);
                        GL.TexCoord2(0.0f, 1.0f); GL.Vertex2(x    , y + 1);
                    }
                    else
                    {
                        GL.TexCoord2(0.0f, 0.0f); GL.Vertex2(x * 2    , y    );
                        GL.TexCoord2(1.0f, 0.0f); GL.Vertex2(x * 2 + 2, y    );
                        GL.TexCoord2(1.0f, 1.0f); GL.Vertex2(x * 2 + 2, y + 1);
                        GL.TexCoord2(0.0f, 1.0f); GL.Vertex2(x * 2    , y + 1);
                    }
                    GL.End();
                }
            }

            // swap buffers and away we go!
            SwapBuffers();
        }

        private Color GetBlinkColor(byte color)
        {
            if(VideoBlink)  // return background color
                return Palette[(color >> 4) - 0x08];
            else            // return foreground color
                return Palette[color & 0x0F];
        }

        private void UpdateZZTVideo()
        {
            string GameName;

            if(ZZT == null || !ZZT.Running)
            {
                this.Exit();
                return;
            }

            GameName = ZZT.Name();
            if(GameName != CurrentName)
            {
                CurrentName = GameName;
                this.Title = "Lyon - [" + GameName + "]";
            }

            VideoTick++;
            if((VideoTick % 15) == 0)
            {
                VideoBlink = !VideoBlink;
                Video.SetBlink(VideoBlink);
                VideoTick = 0;
            }
        }

        /// <summary>
        /// Respond to window resize events.
        /// </summary>
        protected override void OnResize(EventArgs e)
        {
            GL.Viewport(0, 0, Width, Height);
            GL.MatrixMode(MatrixMode.Projection);
            GL.LoadIdentity();
            GL.Ortho(0.0, 80.0, 25.0, 0.0, 0.0, 255.0);
        }

        /// <summary>
        /// Handles unloading of resources.
        /// </summary>
        protected override void OnUnload(EventArgs e)
        {
            if(Initialized)
            {
                GL.DeleteTextures(NUM_GLYPHS, Glyphs);
            }
        }

        /// <summary>
        /// Main game loop.
        /// </summary>
        /// <param name="e">Contains the amount of time since the last update.</param>
        protected override void OnUpdateFrame(FrameEventArgs e)
        {
            if(ZZT != null && ZZT.Running && !ZZT.Busy)
            {
                if(Debugger.IsAttached)
                    ZZT.Execute();
                else
                {
                    try
                    {
                        ZZT.Execute();
                    }
                    catch(Exception ex)
                    {
                        Common.ErrorMessage = ex.Message;
                        Common.ErrorStackTrace = ex.StackTrace;
                        Common.DisplayError(this);
                        ZZT = null;
                    }
                }
            }
            else if(!ZZT.Running)
            {
                this.Exit();
            }
        }

        private void LoadConfig()
        {
            /*
             * Video Settings
            */
            VideoBlinkOn = Convert.ToBoolean(Convert.ToInt32(ConfigManager.GetValue("Video", "Blink", "1")));
            VideoFont = ConfigManager.GetValue("Video", "Font");
            VideoPalette = ConfigManager.GetValue("Video", "Palette");
            VideoScale = Convert.ToDecimal(ConfigManager.GetValue("Video", "Scale", "1"));

            /*
             * Audio Settings
             */
            AudioEnabled = Convert.ToBoolean(Convert.ToInt32(ConfigManager.GetValue("Audio", "Enabled", "1")));
        }

        private void RegisterEvents()
        {
            Keyboard.KeyDown += Keyboard_KeyDown;
            Keyboard.KeyRepeat = true;
        }

        void Keyboard_KeyDown(object sender, KeyboardKeyEventArgs e)
        {
            switch(e.Key)
            {
                case Key.I:
                    ZZT.ToggleFlag("I");
                    break;
                case Key.Number1:
                    if(Keyboard[Key.ControlLeft] || Keyboard[Key.ControlRight])
                        this.ClientSize = new Size(Video.Bitmap.Width, Video.Bitmap.Height);
                    break;
                case Key.Number2:
                    if(Keyboard[Key.ControlLeft] || Keyboard[Key.ControlRight])
                        this.ClientSize = new Size(Video.Bitmap.Width * 2, Video.Bitmap.Height * 2);
                    break;
                default:
                    break;
            }
            Input.KeyDown(RemapKey(e.Key));
        }

        private bool InitializeGame()
        {
            Cache InitCache = Common.LoadWorld();

            if(InitCache == null)
                return false;
            else
            {
                GameWidth = 80;
                GameHeight = 25;
                if(Common.WideMode)
                    GameWidth /= 2;

                Video = new TextModeVideo(new Size(GameWidth, GameHeight), null, null, Common.WideMode);

                if(VideoFont != "")
                {
                    FontData font = FontLoader.GetFontData(VideoFont);
                    Video.SetFont(font.Usable ? font.FontBitmap : null);
                }

                if(VideoPalette != "")
                {
                    PaletteData palette = PaletteLoader.GetPaletteData(VideoPalette);
                    Video.SetPalette(palette.Usable ? palette.Palette : null);
                }

                if(VideoBlinkOn)
                    Video.SetBright(false);
                else
                    Video.SetBright(true);

                Input = new BufferInput();

                if(AudioEnabled)
                    Audio = new AudioOpenAL(44100, Gargoyle.Roton.Audio.BitDepth.Signed16Bit, 2048);
                else
                    Audio = new Audio();

                ZZT = new Game(Video, Audio, Input, InitCache, Common.EngineID, true);
                ClientSize = new Size((int)(Video.Bitmap.Width * VideoScale), (int)(Video.Bitmap.Height * VideoScale));
                ZZT.Load(Common.GameFile);
            }

            return true;
        }

        private Keys RemapKey(Key key)
        {
            Keys outKey;

            switch(key)
            {
                case Key.Up:            outKey = Keys.Up; break;
                case Key.Down:          outKey = Keys.Down; break;
                case Key.Left:          outKey = Keys.Left; break;
                case Key.Right:         outKey = Keys.Right; break;
                case Key.A:             outKey = Keys.A; break;
                case Key.B:             outKey = Keys.B; break;
                case Key.C:             outKey = Keys.C; break;
                case Key.D:             outKey = Keys.D; break;
                case Key.E:             outKey = Keys.E; break;
                case Key.F:             outKey = Keys.F; break;
                case Key.G:             outKey = Keys.G; break;
                case Key.H:             outKey = Keys.H; break;
                case Key.I:             outKey = Keys.I; break;
                case Key.J:             outKey = Keys.J; break;
                case Key.K:             outKey = Keys.K; break;
                case Key.L:             outKey = Keys.L; break;
                case Key.M:             outKey = Keys.M; break;
                case Key.N:             outKey = Keys.N; break;
                case Key.O:             outKey = Keys.O; break;
                case Key.P:             outKey = Keys.P; break;
                case Key.Q:             outKey = Keys.Q; break;
                case Key.R:             outKey = Keys.R; break;
                case Key.S:             outKey = Keys.S; break;
                case Key.T:             outKey = Keys.T; break;
                case Key.U:             outKey = Keys.U; break;
                case Key.V:             outKey = Keys.V; break;
                case Key.W:             outKey = Keys.W; break;
                case Key.X:             outKey = Keys.X; break;
                case Key.Y:             outKey = Keys.Y; break;
                case Key.Z:             outKey = Keys.Z; break;
                case Key.Slash:         outKey = Keys.OemQuestion; break;
                case Key.Escape:        outKey = Keys.Escape; break;
                case Key.Enter:         outKey = Keys.Enter; break;
                case Key.Space:         outKey = Keys.Space; break;
                case Key.BackSpace:     outKey = Keys.Back; break;
                case Key.Minus:         outKey = Keys.OemMinus; break;
                case Key.Plus:          outKey = Keys.Oemplus; break;
                case Key.Period:        outKey = Keys.OemPeriod; break;
                case Key.Comma:         outKey = Keys.Oemcomma; break;
                case Key.BracketLeft:   outKey = Keys.OemOpenBrackets; break;
                case Key.Semicolon:     outKey = Keys.OemSemicolon; break;
                case Key.Quote:         outKey = Keys.OemQuotes; break;
                case Key.PageUp:        outKey = Keys.PageUp; break;
                case Key.PageDown:      outKey = Keys.PageDown; break;
                case Key.Home:          outKey = Keys.Home; break;
                case Key.End:           outKey = Keys.End; break;
                case Key.Insert:        outKey = Keys.Insert; break;
                case Key.Delete:        outKey = Keys.Delete; break;
                case Key.BackSlash:     outKey = Keys.OemBackslash; break;
                case Key.Tab:           outKey = Keys.Tab; break;
                case Key.F1:            outKey = Keys.F1; break;
                case Key.F2:            outKey = Keys.F2; break;
                case Key.F3:            outKey = Keys.F3; break;
                case Key.F4:            outKey = Keys.F4; break;
                case Key.F5:            outKey = Keys.F5; break;
                case Key.F6:            outKey = Keys.F6; break;
                case Key.F7:            outKey = Keys.F7; break;
                case Key.F8:            outKey = Keys.F8; break;
                case Key.F9:            outKey = Keys.F9; break;
                case Key.F10:           outKey = Keys.F10; break;
                case Key.F11:           outKey = Keys.F11; break;
                case Key.F12:           outKey = Keys.F12; break;
                case Key.Number0:       outKey = Keys.NumPad0; break;
                case Key.Number1:       outKey = Keys.NumPad1; break;
                case Key.Number2:       outKey = Keys.NumPad2; break;
                case Key.Number3:       outKey = Keys.NumPad3; break;
                case Key.Number4:       outKey = Keys.NumPad4; break;
                case Key.Number5:       outKey = Keys.NumPad5; break;
                case Key.Number6:       outKey = Keys.NumPad6; break;
                case Key.Number7:       outKey = Keys.NumPad7; break;
                case Key.Number8:       outKey = Keys.NumPad8; break;
                case Key.Number9:       outKey = Keys.NumPad9; break;
                case Key.KeypadPlus:    outKey = Keys.Add; break;
                case Key.KeypadMinus:   outKey = Keys.Subtract; break;
                case Key.KeypadMultiply:outKey = Keys.Multiply; break;
                case Key.KeypadDivide:  outKey = Keys.Divide; break;
                case Key.KeypadDecimal: outKey = Keys.Decimal; break;
                case Key.KeypadEnter:   outKey = Keys.Enter; break;
                default:                outKey = Keys.None; break;
            }

            if(Keyboard[Key.ControlLeft] || Keyboard[Key.ControlRight])
                outKey |= Keys.Control;
            if(Keyboard[Key.AltLeft] || Keyboard[Key.AltRight])
                outKey |= Keys.Alt;
            if(Keyboard[Key.ShiftLeft] || Keyboard[Key.ShiftRight])
                outKey |= Keys.Shift;

            return outKey;
        }
    }
}
